package com.appistry.maven.plugin.far.remote.ssh;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * This class handles execution of commands over SSH. It uses the JSch library,
 * specifically the {@link com.jcraft.jsch.ChannelExec ChannelExec} class to
 * execute the commands on a remote host
 * 
 * <p>
 * TODO: Need to process list of commands in sequential order (see unused
 * method: <code>executeCommand(List&lt;String&gt; commands)</code>)
 * <p>
 * TODO: And therefore need to add compensating (transactional) behavior. For
 * example, if 3 commands were passed in a list, and the last one failed, then
 * the orchestrator needs to call compensating operations on the commands that
 * succeeded.
 * <p>
 * TODO: And then finally, need to allow for input of commands for
 * configurability
 * 
 * @see com.jcraft.jsch.ChannelExec
 * @see com.jcraft.jsch.Session
 */
public class SshExec implements JSchWrapper {

	private SshConfig sshConfig;
	private Session session;
	private int exitStatus = -1;
	private String result; // should go in a Command class - since this is the
							// result of the command
	private String error; // should go in a Command class

	public SshConfig getSshConfig() {
		return sshConfig;
	}

	public void setSshConfig(SshConfig sshConfig) {
		this.sshConfig = sshConfig;
	}

	public int getExitStatus() {
		return exitStatus;
	}

	public void setExitStatus(int exitStatus) {
		this.exitStatus = exitStatus;
	}

	public String getResult() {
		return result;
	}

	public void setResult(String result) {
		this.result = result;
	}

	public String getError() {
		return error;
	}

	public void setError(String error) {
		this.error = error;
	}

	public SshExec() {
		super();
	}

	public SshExec(SshConfig sshConfig) {
		this.sshConfig = sshConfig;
	}

	/**
	 * This method opens a ssh session to the remote host, executes the command
	 * over a channel, and closes the channel and session when finished
	 * 
	 * @param command
	 * @throws JSchException
	 */
	public void executeCommand(String command) throws JSchException {
		System.out.println("SshExec.executeCommand - START");

		try {

			// get a valid JSch Session
			openSshSession();

			// execute command
			execute(command.trim());

		} finally {
			// cleanup
			closeSshSession();
		}
	}

	// FIXME: Need to figure out how to send multiple commands
	// "executed in sequence"
	// For example:
	// $ cd rosh
	// rosh$ ls
	// rosh$ cd ..
	// $
	public void executeCommand(List<String> commands) throws JSchException {
		System.out.println("SshExec.executeCommand - START");

		try {

			// get a valid JSch Session
			openSshSession();

			// execute command
			for (String command : commands) {
				execute(command.trim());
			}

		} finally {
			// cleanup
			closeSshSession();
		}
	}

	private void execute(final String command) throws JSchException {
		System.out.println("SshExec.execute - START");

		Channel channel = null;
		ByteArrayOutputStream baosOut = new ByteArrayOutputStream(1024);
		ByteArrayOutputStream baosErr = new ByteArrayOutputStream(1024);

		try {
			channel = this.session.openChannel(CHANNEL_SSH);
			System.out.println("Open 'exec' Channel");

			((ChannelExec) channel).setCommand(command);
			System.out.println("Set command " + command + " in channel");

			// runCommandAlternativeApproach(channel);

			channel.setOutputStream(baosOut);
			((ChannelExec) channel).setExtOutputStream(baosOut);
			((ChannelExec) channel).setErrStream(baosErr);

			channel.connect();
			try {
				waitForCommandToFinishExecution(channel);
			} catch (InterruptedException e) {
				// ignore for now since even is the thread is interrupted, I'm
				// checking if the channel is close and setting exitStatus (by
				// default it is set to -1 [Failure])
				e.printStackTrace();
			}

			if (channel.isClosed()) {
				setExitStatus(channel.getExitStatus());
			}
			System.out.println("exit-status: " + exitStatus);

			if (exitStatus != 0) {
				/*
				 * TODO: if failOnError is set to true then throw a
				 * RuntimeException with error message. If false, then just
				 * print out a WARNing message
				 */
				String remoteError = baosErr.toString();
				System.err
						.println("Error occurred when running command remotely on "
								+ sshConfig.getHost() + ": " + remoteError);

				if (baosErr.size() > 0) {
					setResult(remoteError);
				}

				throw new RuntimeException(remoteError);
			}

			setResult(baosOut.toString());
			System.out.println("Result: " + getResult());

		} finally {
			// cleanup

			if (channel != null && !channel.isClosed()) {
				channel.disconnect();
				channel = null;
			}

			try {
				baosOut.close();
				baosOut = null;
			} catch (IOException ioe) {
				System.err
						.println("Exception encountered closing channel output stream");
				ioe.printStackTrace();
			}

			try {
				baosErr.close();
				baosErr = null;
			} catch (IOException ioe) {
				System.err
						.println("Exception encountered closing channel error stream");
				ioe.printStackTrace();
			}

		}
	}

	private void closeSshSession() {
		if (session != null && session.isConnected()) {
			session.disconnect();
		}
	}

	private void openSshSession() throws JSchException {

		if (session == null || !session.isConnected()) {
			JSch jsch = new JSch();
			session = jsch.getSession(sshConfig.getSshUserInfo().getUsername(),
					sshConfig.getHost(), sshConfig.getPort());
		}

		// assign UserInfo
		session.setUserInfo(sshConfig.getSshUserInfo());

		// session connect
		if (!session.isConnected()) {
			session.connect();
		}
		System.out.println("Session connected");

	}

	private void waitForCommandToFinishExecution(final Channel channel)
			throws InterruptedException {
		Thread thread = new Thread("Waiting for command execution to complete") {

			public void run() {
				while (!channel.isClosed()) {
					try {
						sleep(100);
					} catch (InterruptedException e) {
						// ignored
					}
				}
			}
		};
		thread.start();
		thread.join(0);
	}

	private void runCommandAlternativeApproach(Channel channel)
			throws JSchException {
		channel.setInputStream(null);
		((ChannelExec) channel).setErrStream(System.err);

		try {
			InputStream in = channel.getInputStream();

			channel.connect();

			byte[] tmp = new byte[1024];
			while (true) {
				while (in.available() > 0) {
					int i = in.read(tmp, 0, 1024);
					if (i < 0)
						break;
					System.out.print(new String(tmp, 0, i));
				}
				if (channel.isClosed()) {
					System.out.println("exit-status: "
							+ channel.getExitStatus());
					break;
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException ie) {
					// ignore
				}
			}
		} catch (IOException ioe) {
			System.err.println("IOException occured: "
					+ ioe.getLocalizedMessage());
		}
	}

}
